using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Threading;
using System.IO;

using InvocationFramework.Common;
using InvocationFramework.Common.Instrumentation;
using InvocationFramework.Common.Exceptions;
using InvocationFramework.Common.Library;
using InvocationFramework.Common.Resources;

using InvocationFramework.Adapters.Inproc.Attributes;
using InvocationFramework.Adapters.Inproc.Transactions;

namespace InvocationFramework.Adapters.Inproc
{
    /// <summary>
    /// Catalogo en memoria de los servicios que puede ejecutar el adaptador <seealso cref="CatalogManager"/>
    /// </summary>
    /// <author>Rodolfo Finochietti 01/11/2005 13.18</author>
    internal class CatalogManager : MarshalByRefObject
    {
        #region Private Vars

        /// <summary>
        /// Directorio donde se encuentran los servicios
        /// </summary>
        private string _servicesFolder;

        /// <summary>
        /// Directorio temporal de ejecucion de los servicios
        /// </summary>
        private string _workingFolder;

        /// <summary>
        /// Esta coleccion guarda el catalog de servicios. Cada servicio en la coleccion tiene un catalogo de operaciones asociado.
        /// </summary>
        private Dictionary<string, Dictionary<string, OperationDefinition>> _servicesCatalog = new Dictionary<string, Dictionary<string, OperationDefinition>>( );

        /// <summary>
        /// Objeto usado para sincronizacion
        /// </summary>
        private ReaderWriterLock syncServicesCatalog = new ReaderWriterLock( );

        /// <summary>
        /// Objeto usado para sincronizacion
        /// </summary>
        private object syncRoot = new object( );

        #endregion

        #region Constructors

        /// <summary>
        /// Constructor por defecto
        /// </summary>
        public CatalogManager( )
        {
            //En el catalogo tambien debo iniciar el resolvedor por que si esta habilitado el hot deploy
            //el catalolo se corre en otro app domain
            AssemblyResolver.Start( );
        }

        #endregion

        #region Properties

        /// <summary>
        /// Variable privada para el manejo de la propiedad
        /// </summary>
        private List<string> _mostRequestedServices = new List<string>( );

        /// <summary>
        /// Array con los servicios mas utilizados dentro del catalogo
        /// </summary>
        public string[] MostRequestedServices
        {
            get
            {
                return _mostRequestedServices.ToArray( );
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Verifica si un servicio esta en el catalogo
        /// </summary>
        /// <param name="serviceName">Nombre del servicio</param>
        /// <returns>True si el servicio se encuentra en el catalogo</returns>
        public bool ContainsService( string serviceName )
        {
            bool contains;

            syncServicesCatalog.AcquireReaderLock( Constants.LockTimeout );
            try
            {
                contains = _servicesCatalog.ContainsKey( serviceName );
            }
            finally
            {
                syncServicesCatalog.ReleaseReaderLock( );
            }

            return contains;
        }

        /// <summary>
        /// Verifica si existe una operacion en un servicio
        /// </summary>
        /// <param name="serviceName">Nombre del servicio</param>
        /// <param name="operationName">Nombre de la operacion</param>
        /// <returns>True si la operacion existe en el servicio, de lo contrario False</returns>
        public bool ContainsServiceOperation( string serviceName, string operationName )
        {
            bool result;

            syncServicesCatalog.AcquireReaderLock( Constants.LockTimeout );
            try
            {
                result = ( _servicesCatalog.ContainsKey( serviceName ) && _servicesCatalog[ serviceName ].ContainsKey( operationName ) );
            }
            finally
            {
                syncServicesCatalog.ReleaseReaderLock( );
            }

            return result;
        }

        /// <summary>
        /// Devuelve la definicion de la operacion de un servicio
        /// </summary>
        /// <param name="serviceName">Nombre del servicio</param>
        /// <param name="operationName">Nombre de la operacion</param>
        /// <returns>La defincion si pudo ser encontrada, null de lo contrario</returns>
        public OperationDefinition GetServiceOperationDefinition( string serviceName, string operationName )
        {
            OperationDefinition result = null;

            syncServicesCatalog.AcquireReaderLock( Constants.LockTimeout );
            try
            {
                if ( ContainsServiceOperation( serviceName, operationName ) )
                    result = _servicesCatalog[ serviceName ][ operationName ];
            }
            finally
            {
                syncServicesCatalog.ReleaseReaderLock( );
            }

            return result;
        }

        /// <summary>
        /// Carga el catalogo de servicios
        /// </summary>
        /// <param name="servicesFolder">Directorio donde estan los assemblys con los servicios</param>
        /// <param name="workingFolder">Carpeta temporal de trabajo</param>
        public void PopulateCatalog( string servicesFolder, string workingFolder )
        {
            if ( _servicesFolder != null )
                throw new InvocationFrameworkException( Texts.CatalogManagerAlreadyPopulated );

            try
            {
                lock ( syncRoot )
                {
                    //Seteo las propiedades
                    _servicesFolder = servicesFolder;
                    _workingFolder = workingFolder;

                    //Configura la carpepa del resolvedor de assemblies (por si se usara)
                    AssemblyResolver.AssembliesFolder = workingFolder;

                    //Copio los archivos al directorio temporal
                    CopyServicesToTemp( );

                    //Busco los assemblys marcados como repositorios de servicios en la carpeta de servicios configurada
                    Assembly[] repositorys = ReflectionHelper.GetAssemblys( _workingFolder, typeof( ServiceRepositoryAttribute ) );
                    foreach ( Assembly repository in repositorys )
                        AddServiceToCatalog( repository );
                }
            }
            catch ( Exception ex )
            {
                TraceHelper.Error( ex, Texts.CatalogManagerPopulateCatalogError );
                throw;
            }
        }

        /// <summary>
        /// Agrega un assembly al catalogo, siempre y cuando el assembly este decorado con los atributos correctos
        /// </summary>
        /// <param name="assemblyFile">Nombre completo del archivo a agregar</param>
        public void AddAssemblyToCatalog( string assemblyFile )
        {
            lock ( syncServicesCatalog )
            {
                AddServiceToCatalog( ReflectionHelper.GetAssembly( assemblyFile, typeof( ServiceRepositoryAttribute ) ) );
            }
        }

        /// <summary>
        /// Ejecuta un servicio del catalogo
        /// </summary>
        /// <param name="serviceName">Nombre del servicio</param>
        /// <param name="operationName">Nombre de la operacion</param>
        /// <param name="parameters">Parametros de la operacion</param>
        /// <returns>Resultado de la ejecucion de la operacion</returns>
        public object Execute( string serviceName, string operationName, params object[] parameters )
        {
            object result = null;

            //Obtengo la definicion de la operacion
            OperationDefinition operDef = GetServiceOperationDefinition( serviceName, operationName );

            //Si existe la operacion en el catalogo la ejecuto
            if ( operDef != null )
            {
                //Ejecuto el metodo que representa la operacion, si la operacion esta marcada como transaccional la ejecucion es dentro de un transaccion COM+
                if ( operDef.TransactionType == TransactionScope.Disabled )
                    result = OperationExecutor.Execute( operDef, parameters );
                else
                    result = TransactionDispatcher.Execute( operDef, parameters );
            }

            //Devulevo el resultado de la operacion
            return result;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Agrega al catalogo los servicios que se encuentran en el assembly pasado por parametro
        /// </summary>
        /// <param name="servicesRepository">El assembly que contiene los servicios a agregar al catalogo</param>
        private void AddServiceToCatalog( Assembly servicesRepository )
        {
            if ( servicesRepository == null )
                return;

            //Recorro los tipos que hay en el assembly
            Type[] services = servicesRepository.GetTypes( );
            foreach ( Type service in services )
            {
                //Si el tipo no es una clase no se chequea
                if ( service.IsClass )
                {
                    //Chequeo si la clase esta marcada como servicio y si es asi la agrego al catalogo
                    CheckTypeForService( service, service );

                    //Chequeo si alguna de las interfaces de la clase esta marcada como servicio y
                    //si es asi lo agrego al catalogo
                    CheckInterfacesForService( service );
                }
            }
        }

        /// <summary>
        /// Cheque si en las interfaces de un tipo esta definido un servicio
        /// </summary>
        /// <param name="service">Tipo cuyas interfaces se van a chequear</param>
        private void CheckInterfacesForService( Type service )
        {
            //Si el tipo no es una clase no se chequea
            if ( !service.IsClass )
                return;

            //Busco las interfaces de la clase
            Type[] interfaces = service.GetInterfaces( );
            foreach ( Type serviceInteface in interfaces )
                //Chequeo si la interfaz esta marcada como servicio y si es asi la agrego al catalogo
                CheckTypeForService( serviceInteface, service ); //El tipo que se agrega al catalogo es la clase que implementa la interfaz (no la interfaz)
        }

        /// <summary>
        /// Chequea si un tipo esta marcado como servicio
        /// </summary>
        /// <param name="typeToCheck">El tipo a chequear</param>
        /// <param name="typeToAdd">El tipo a agregar al catalogo de servicios</param>
        private void CheckTypeForService( Type typeToCheck, Type typeToAdd )
        {
            //Chequeo que el tipo este marcado como servicio
            object[] attributes = typeToCheck.GetCustomAttributes( typeof( ServiceAttribute ), true );
            if ( ( attributes != null && attributes.GetUpperBound( 0 ) >= 0 ) )
            {
                //Saco el nombre del servicio de sus atributos
                string serviceName = ( ( ServiceAttribute )attributes[ 0 ] ).Name;

                //Agrego el servicio al repositorio de servicios muy utilizados (si lo fuera)
                AddServiceToMostRequestedCatalog( serviceName, typeToCheck );

                //Si el tipo esta marcado busco los metodos marcados como operaciones y los agrego al catalogo
                MethodInfo[] methods = typeToCheck.GetMethods( );
                foreach ( MemberInfo method in methods )
                    AddOperationToCatalog( serviceName, typeToAdd, method );
            }
        }

        /// <summary>
        /// Controla si un tipo esta marcado como un servicio muy utilizado y si es asi lo agrega al catalogo de servicios muy utilizados
        /// </summary>
        /// <param name="serviceName">Nombre del servicio</param>
        /// <param name="service">Tipo que contiene al servicio</param>
        /// <remarks>
        /// Este metodo no chequea la presencia del attributo <seealso cref="ServiceAttribute"/>
        /// </remarks>
        private void AddServiceToMostRequestedCatalog( string serviceName, Type service )
        {
            object[] attributes = service.GetCustomAttributes( typeof( MostRequestedServiceAttribute ), true );
            if ( attributes != null && attributes.GetUpperBound( 0 ) >= 0 )
            {
                if ( !_mostRequestedServices.Contains( serviceName ) )
                {
                    _mostRequestedServices.Add( serviceName );

                    TraceHelper.Verbose( String.Format( CultureInfo.InvariantCulture, Texts.CatalogManagerMostRequestedServiceCatalogTrace, serviceName ) );
                }
            }
        }

        /// <summary>
        /// Agrega una operacion al catalogo
        /// </summary>
        /// <param name="serviceName">Nombre del servicio</param>
        /// <param name="service">Tipo del servicio</param>
        /// <param name="method">Metodo donde esta implementada la operacion</param>
        private void AddOperationToCatalog( string serviceName, Type service, MemberInfo method )
        {
            //Controlo que el metodo este marcado como operacion
            object[] operationAttribute = method.GetCustomAttributes( typeof( OperationAttribute ), true );
            if ( operationAttribute != null && operationAttribute.GetUpperBound( 0 ) >= 0 )
            {
                //Si el metodo es una operacion de un servicio lo agrego al catalogo
                string operationName = ( ( OperationAttribute )operationAttribute[ 0 ] ).Name;
                string methodName = method.Name;

                //Creo una definicion para esta operacion
                OperationDefinition operDef = new OperationDefinition( );
                operDef.ServiceName = serviceName;
                operDef.Name = operationName;
                operDef.OperationType = service;
                operDef.OperationMethod = methodName;

                //Me fijo si el servicio tiene el atributo de transacciones
                object[] transactionContextAttribute = method.GetCustomAttributes( typeof( TransactionAttribute ), true );
                if ( transactionContextAttribute != null && transactionContextAttribute.GetUpperBound( 0 ) >= 0 )
                {
                    TransactionAttribute tranAttrib = ( ( TransactionAttribute )transactionContextAttribute[ 0 ] );
                    operDef.TransactionType = tranAttrib.TransactionType;
                    operDef.TransactionIsolation = tranAttrib.TransactionIsolation;
                    operDef.TransactionTimeout = tranAttrib.TransactionTimeout;
                }

                //Si no hay un catalogo de operaciones para el servicio (por que es la primera operacion que se agrega) creo uno
                Dictionary<string, OperationDefinition> operations;

                syncServicesCatalog.AcquireReaderLock( Constants.LockTimeout );
                try
                {
                    if ( _servicesCatalog.ContainsKey( serviceName ) )
                        operations = _servicesCatalog[ serviceName ];
                    else
                        operations = new Dictionary<string, OperationDefinition>( );

                    //Agrego la operacion al catalogo de operaciones y actualizo el catalogo de servicios
                    operations[ operationName ] = operDef;

                    LockCookie cookie = syncServicesCatalog.UpgradeToWriterLock( Constants.LockTimeout );
                    try
                    {
                        _servicesCatalog[ serviceName ] = operations;
                    }
                    finally
                    {
                        syncServicesCatalog.DowngradeFromWriterLock( ref cookie );
                    }
                }
                finally
                {
                    syncServicesCatalog.ReleaseReaderLock( );
                }

                TraceHelper.Verbose( String.Format( CultureInfo.InvariantCulture, Texts.CatalogManagerOperationCatalogTrace, operationName, serviceName, service.FullName, methodName ) );
            }
        }

        /// <summary>
        /// Copia los assemblys que se encuentran en el directorio de servicios al directorio temporal
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage( "Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes" )]
        private void CopyServicesToTemp( )
        {
            try
            {
                //Solo se hace la copia si los directorios son distintos
                if ( _workingFolder != _servicesFolder )
                {
                    //Borro todos los archivos en el directorio temporal
                    string[] files = Directory.GetFiles( _workingFolder );
                    foreach ( string file in files )
                        File.Delete( file );

                    //Copio los servicios al directorio temporal
                    files = Directory.GetFiles( _servicesFolder );
                    foreach ( string file in files )
                        File.Copy( file, Path.Combine( _workingFolder, Path.GetFileName( file ) ) );
                }
            }
            //Si se produce un error asumo que es por que hay otra instancia del catalog manager operando
            //sobre los mismo directorios. Este caso se da cuando dentro de un servicio inproc se el
            //service caller para invocar otro servicio inproc con un adaptador configurado en los mismos
            //directorios
            catch ( Exception ex )
            {
                TraceHelper.Warning( ex, String.Format( CultureInfo.InvariantCulture, Texts.CatalogManagerServiceCopyFail ) );
            }
        }

        #endregion
   }
}